/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.gateway.server.util;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.iwindplus.base.domain.constant.CommonConstant;
import com.iwindplus.base.domain.constant.CommonConstant.HeaderConstant;
import com.iwindplus.base.domain.constant.CommonConstant.NetWorkConstant;
import com.iwindplus.base.domain.constant.CommonConstant.NumberConstant;
import com.iwindplus.base.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.base.domain.constant.CommonConstant.SystemConstant;
import com.iwindplus.base.domain.context.UserContextHolder;
import com.iwindplus.base.domain.vo.UserBaseVO;
import com.iwindplus.base.util.DatesUtil;
import com.iwindplus.base.util.HttpsUtil;
import com.iwindplus.base.util.JacksonUtil;
import com.iwindplus.base.util.ReactorUtil;
import com.iwindplus.base.util.UrlUtil;
import com.iwindplus.base.util.domain.dto.ReactorRequestDTO;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.ServerWebExchangeContextConstant;
import com.iwindplus.gateway.server.domain.property.GatewayProperty.LogConfig;
import com.iwindplus.gateway.server.support.GatewayLogPublisher;
import com.iwindplus.log.domain.dto.GatewayLogDTO;
import com.iwindplus.log.domain.dto.GatewayLogDTO.GatewayLogDTOBuilder;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

/**
 * Gateway 工具类
 *
 * @author zengdegui
 * @since 2024/05/22 22:22
 */
@Slf4j
public class GatewayUtil {

    private GatewayUtil() {
        throw new IllegalStateException(CommonConstant.UTILITY_CLASS);
    }

    /**
     * 获取API前缀（路由）.
     *
     * @param exchange 请求
     * @return String
     */
    public static String getApiPrefix(ServerWebExchange exchange) {
        String remaining = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ATTR);
        if (CharSequenceUtil.isBlank(remaining)) {
            return SymbolConstant.EMPTY_STR;
        }

        return remaining.replace(SymbolConstant.BASE_PATH, SymbolConstant.EMPTY_STR)
            .replace(SymbolConstant.SLASH_ASTERISK, SymbolConstant.EMPTY_STR);
    }

    /**
     * 获取请求相对路径.
     *
     * @param exchange 请求
     * @return String
     */
    public static String getRelativePath(ServerWebExchange exchange) {
        final ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().value();
        String apiPrefix = request.getHeaders().getFirst(HeaderConstant.X_FORWARDED_PREFIX);
        if (CharSequenceUtil.isBlank(apiPrefix)) {
            apiPrefix = GatewayUtil.getApiPrefix(exchange);
        }

        return CharSequenceUtil.isNotBlank(apiPrefix)
            ? requestPath.substring(apiPrefix.length())
            : requestPath;
    }

    /**
     * 获取真实ip.
     *
     * @param exchange 请求
     * @return String
     */
    public static String getRealIp(ServerWebExchange exchange) {
        XForwardedRemoteAddressResolver xForwardedRemoteAddressResolver = XForwardedRemoteAddressResolver.maxTrustedIndex(1);
        InetSocketAddress inetSocketAddress = xForwardedRemoteAddressResolver.resolve(exchange);
        return Optional.ofNullable(inetSocketAddress).map(InetSocketAddress::getAddress).map(InetAddress::getHostAddress)
            .orElse(Optional.ofNullable(exchange.getRequest().getRemoteAddress())
                .map(InetSocketAddress::getHostString)
                .orElse(SystemConstant.UNKNOWN));
    }

    /**
     * 获取语言.
     *
     * @param req 请求
     * @return String
     */
    public static String getLang(ServerHttpRequest req) {
        return Optional.ofNullable(req.getHeaders().getFirst(HttpHeaders.ACCEPT_LANGUAGE))
            .orElse(HttpsUtil.buildDefaultLanguage());
    }

    /**
     * 构建网关日志信息.
     *
     * @param exchange 请求
     * @param cfg      日志配置
     * @return Mono<GatewayLogDTO>
     */
    public static Mono<GatewayLogDTO> buildGatewayLog(ServerWebExchange exchange, LogConfig cfg) {
        final UserBaseVO user = ReactorUtil.getAttribute(exchange, ServerWebExchangeContextConstant.USER_INFO);
        final long requestTime = ReactorUtil.getAttribute(exchange, ServerWebExchangeContextConstant.REQUEST_TIME);
        final ServerHttpRequest request = exchange.getRequest();
        final HttpHeaders headers = request.getHeaders();

        GatewayLogDTOBuilder<?, ?> builder = GatewayLogDTO.builder()
            .bizTraceId(MDC.get(HeaderConstant.TRACE_ID))
            .ip(headers.getFirst(HeaderConstant.REAL_IP))
            .requestSchema(request.getURI().getScheme())
            .requestPath(request.getPath().value())
            .requestMethod(request.getMethod().name())
            .requestTime(DatesUtil.parseDate(requestTime, DatePattern.NORM_DATETIME_MS_PATTERN));

        GatewayUtil.buildTargetServer(exchange, builder);
        GatewayUtil.buildUserInfo(builder, user);
        GatewayUtil.buildUserAgent(builder, headers.getFirst(HttpHeaders.USER_AGENT));

        // 2. 真正需要 RequestBody 时才去“读一次”，并且一直在 Mono 里
        return ReactorUtil.getRequestBodyByAttr(exchange)
            .filter(data -> cfg.getEnabledRequestHeader()
                || cfg.getEnabledRequestBody()
                || cfg.getEnabledRequestParam())
            .map(data -> buildRequestData(cfg, builder, data))
            .switchIfEmpty(Mono.<GatewayLogDTO>fromCallable(() -> builder.build()));
    }

    /**
     * 异步发布日志.
     *
     * @param entity 日志信息
     * @return void
     */
    public static void asyncPublishGatewayLog(GatewayLogDTO entity) {
        try {
            final long beginMillis = DatesUtil.parse(entity.getRequestTime(), DatePattern.NORM_DATETIME_MS_PATTERN).getTime();
            final long endMillis = System.currentTimeMillis();
            entity.setResponseTime(DatesUtil.parseDate(endMillis, DatePattern.NORM_DATETIME_MS_PATTERN));
            entity.setExecuteTime(endMillis - beginMillis);
            SpringUtil.getBean(GatewayLogPublisher.class).publish(entity);
        } catch (Exception e) {
            log.error("Failed to publish gateway log event", e);
        }
    }

    /**
     * 异步发布错误日志（记录异常日志）.
     *
     * @param exchange       请求
     * @param cfg            配置
     * @param responseStatus 响应状态
     * @return Mono<Void>
     */
    public static Mono<Void> asyncPublishErrorLog(ServerWebExchange exchange, LogConfig cfg, Integer responseStatus) {
        if (!cfg.getEnabledError()) {
            return Mono.empty();
        }

        return GatewayUtil.buildGatewayLog(exchange, cfg)
            .publishOn(Schedulers.boundedElastic())
            .doOnNext(dto -> {
                dto.setResponseStatus(responseStatus);
                asyncPublishGatewayLog(dto);
            }).then();
    }

    private static void buildTargetServer(ServerWebExchange exchange, GatewayLogDTOBuilder<?, ?> builder) {
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        if (route == null) {
            builder.targetServer(SystemConstant.UNKNOWN).requestSchema(SystemConstant.UNKNOWN);
            return;
        }

        URI uri = route.getUri();
        // 1. 优先取 host
        String target = uri.getHost();
        // 2. host 为空时兜底解析
        if (CharSequenceUtil.isBlank(target)) {
            target = uri.toString()
                .replaceFirst("^.+://([^/:]+).*", "$1");

            if (uri.toString().contains(NetWorkConstant.WSS_PREFIX)) {
                builder.requestSchema(NetWorkConstant.WSS);
            } else if (uri.toString().contains(NetWorkConstant.WS_PREFIX)) {
                builder.requestSchema(NetWorkConstant.WS);
            }
        }

        builder.targetServer(target);
    }

    private static void buildUserInfo(GatewayLogDTOBuilder<?, ?> builder, UserBaseVO user) {
        if (null == user) {
            user = UserContextHolder.getDefaultUser();
        }

        builder.userId(user.getUserId())
            .orgId(user.getOrgId())
            .createdId(user.getUserId())
            .createdBy(user.getRealName())
            .modifiedBy(user.getRealName())
            .modifiedId(user.getUserId());
    }

    private static void buildUserAgent(GatewayLogDTOBuilder<?, ?> builder, String userAgentStr) {
        if (CharSequenceUtil.isBlank(userAgentStr)) {
            return;
        }

        UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
        Optional.ofNullable(userAgent).ifPresent(agent ->
            builder.platformName(agent.getPlatform().getName())
                .osName(agent.getOs().getName())
                .browserName(agent.getBrowser().getName())
        );
    }

    private static GatewayLogDTO buildRequestData(LogConfig cfg, GatewayLogDTOBuilder<?, ?> builder, ReactorRequestDTO data) {
        if (cfg.getEnabledRequestHeader()) {
            Map<String, String> headerMap = data.getRequestHeaders();
            if (MapUtil.isNotEmpty(headerMap)) {
                builder.requestHeaders(JacksonUtil.toJsonStr(headerMap));
            }
        }
        if (cfg.getEnabledRequestBody()) {
            String body = data.getRequestBody();
            if (CharSequenceUtil.isNotBlank(body)) {
                int limit = cfg.getLimitRequestBody() * NumberConstant.NUMBER_ONE_THOUSAND_TWENTY_FOUR;
                builder.requestBody(CharSequenceUtil.maxLength(body, limit));
            }
        }
        if (cfg.getEnabledRequestParam()) {
            Map<String, String> paramMap = data.getQueryParams();
            if (MapUtil.isNotEmpty(paramMap)) {
                builder.requestParam(UrlUtil.buildQuery(paramMap, StandardCharsets.UTF_8));
            }
        }
        return builder.build();
    }

}
